/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.nav.safeargs

import com.android.ide.common.gradle.Version
import com.android.ide.common.repository.GoogleMavenArtifactId.ANDROIDX_NAVIGATION_COMMON
import com.android.tools.idea.testing.AndroidProjectRule
import com.intellij.facet.FacetManager
import com.intellij.testFramework.EdtRule
import com.intellij.testFramework.RunsInEdt
import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture
import org.jetbrains.android.facet.AndroidFacet
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

/**
 * Useful test rule for common setup across most safe args tests, which creates a single module
 * application (with module package "test.safeargs").
 *
 * This rule also enables running tests in the EDT. Apply the [RunsInEdt] annotation either on the
 * test class or individual test method to enable it.
 */
class SafeArgsRule(val mode: SafeArgsMode = SafeArgsMode.JAVA) : TestRule {
  /** Overrides the Android app package name for a given test. */
  annotation class PackageName(val packageName: String)

  private val projectRule =
    AndroidProjectRule.onDisk().apply {
      if (mode == SafeArgsMode.KOTLIN) {
        withKotlin()
      }
    }

  val fixture: JavaCodeInsightTestFixture
    get() = projectRule.fixture as JavaCodeInsightTestFixture

  val androidFacet
    get() = FacetManager.getInstance(projectRule.module).getFacetByType(AndroidFacet.ID)!!

  val module
    get() = androidFacet.module

  val project
    get() = module.project

  fun waitForPendingUpdates() {
    waitForPendingUpdates(projectRule.module)
  }

  private fun setUp(packageName: String) {
    fixture.testDataPath = TestDataPaths.TEST_DATA_ROOT
    fixture.addFileToProject(
      "AndroidManifest.xml",
      """
      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="$packageName">
        <application />
      </manifest>
    """
        .trimIndent(),
    )

    androidFacet.safeArgsMode = mode

    // Add fake "NavArgs" interface to this project so the args class can resolve its interface
    with(
      fixture.addFileToProject(
        "src/androidx/navigation/NavArgs.java",
        // language=java
        """
        package androidx.navigation;

        public interface NavArgs {}
      """
          .trimIndent(),
      )
    ) {
      fixture.allowTreeAccessForFile(this.virtualFile)
    }

    // Add fake "NavDirections" interface to this project so the args class can resolve its
    // interface
    with(
      fixture.addFileToProject(
        "src/androidx/navigation/NavDirections.java",
        // language=java
        """
        package androidx.navigation;

        public interface NavDirections {}
      """
          .trimIndent(),
      )
    ) {
      fixture.allowTreeAccessForFile(this.virtualFile)
    }
  }

  override fun apply(base: Statement, description: Description): Statement {
    // Allow using the @SafeArgsRule.PackageName annotation on a test method to change the package
    // name of the generated Android package.
    val annotation = description.getAnnotation(PackageName::class.java)
    val packageName = annotation?.packageName ?: "test.safeargs"
    val statement =
      object : Statement() {
        override fun evaluate() {
          setUp(packageName)
          base.evaluate()
        }
      }

    // We want to run tests on the EDT thread, but we also need to make sure the project rule is not
    // initialized on the EDT.
    return RuleChain.outerRule(projectRule).around(EdtRule()).apply(statement, description)
  }

  /**
   * Test utility for setting up Safe Args Features as if for a specific [Version] of the
   * [ANDROIDX_NAVIGATION_COMMON] artifact.
   *
   * If this is not called, it is expected that only base functionality will be provided.
   *
   * Finally, this method should be called before any light classes are generated.
   */
  fun setSafeArgsFeatureForVersion(version: Version) {
    val features =
      setOfNotNull(
        SafeArgsFeature.FROM_SAVED_STATE_HANDLE.takeIf {
          version >= SafeArgsFeatureVersions.FROM_SAVED_STATE_HANDLE
        },
        SafeArgsFeature.TO_SAVED_STATE_HANDLE.takeIf {
          version >= SafeArgsFeatureVersions.TO_SAVED_STATE_HANDLE
        },
        SafeArgsFeature.ADJUST_PARAMS_WITH_DEFAULTS.takeIf {
          version >= SafeArgsFeatureVersions.ADJUST_PARAMS_WITH_DEFAULTS
        },
      )
    setSafeArgsFeatures(features)
  }

  /**
   * Test utility for setting up Safe Args Features for our module. This allows testing for the
   * existing / absence of various features. If this is not called (directly or indirectly), only
   * base functionality will be provided.
   *
   * This should be called before any light classes are generated.
   */
  private fun setSafeArgsFeatures(features: Set<SafeArgsFeature>) {
    androidFacet.safeArgsFeatures = features
  }
}

object SafeArgsFeatureVersions {
  val MINIMUM_VERSION = Version.prefixInfimum("0")
  val FROM_SAVED_STATE_HANDLE = Version.parse("2.4.0-alpha01")
  val TO_SAVED_STATE_HANDLE = Version.parse("2.4.0-alpha07")
  val ADJUST_PARAMS_WITH_DEFAULTS = Version.parse("2.4.0-alpha08")
}
